3. Transformação de dados#
Nos capítulos anteriores, aprendemos como usar marcas e codificações visuais para representar registros de dados individuais. Aqui, exploraremos métodos de transformação de dados, incluindo o uso de agregações para resumir múltiplos registros. A transformação de dados é uma parte integral da visualização: escolher quais variáveis exibir, bem como seu nível de detalhamento, é tão importante quanto escolher as codificações visuais apropriadas. Afinal, não importa o quão bem escolhidas sejam suas codificações visuais se você estiver mostrando as informações erradas!
Enquanto percorre este módulo, recomendamos que você abra a documentação de Transformações de Dados do Altair em outra aba. Será um recurso útil caso, em algum momento, você queira mais detalhes ou deseje ver quais outras transformações estão disponíveis.
Este capítulo faz parte do currículo de visualização de dados.
import pandas as pd
import altair as alt
#adicionado só para rodar o código
from vega_datasets import data
movies_url = data.movies()
movies_url
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[2], line 2
1 #adicionado só para rodar o código
----> 2 from vega_datasets import data
3 movies_url = data.movies()
4 movies_url
ModuleNotFoundError: No module named 'vega_datasets'
Histogramas#
Começaremos nosso tour de transformação agrupando os dados em categorias discretas e contando os registros para resumir esses grupos. Os gráficos resultantes são conhecidos como histogramas.
Primeiro, vamos observar os dados não agregados: um gráfico de dispersão que mostra as avaliações de filmes do Rotten Tomatoes em comparação com as avaliações dos usuários do IMDB. Forneceremos os dados ao Altair passando a URL dos dados dos filmes para o método Chart. (Também poderíamos passar diretamente um DataFrame do Pandas para obter o mesmo resultado.) Em seguida, podemos codificar os campos de avaliação do Rotten Tomatoes e do IMDB usando os canais x e y:
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q'),
alt.Y('IMDB_Rating:Q')
)
Para resumir esses dados, podemos agrupar um campo de dados para agrupar valores numéricos em grupos discretos. Aqui, agrupamos ao longo do eixo x adicionando bin=True ao canal de codificação x. O resultado é um conjunto de dez intervalos de mesmo tamanho, cada um correspondendo a um intervalo de dez pontos de avaliação.
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=True),
alt.Y('IMDB_Rating:Q')
)
Definir bin=True usa as configurações padrão de agrupamento, mas podemos ter mais controle, se desejado. Em vez disso, vamos definir o número máximo de intervalos (maxbins) como 20, o que tem o efeito de dobrar a quantidade de grupos. Agora, cada intervalo corresponde a um espaço de cinco pontos de avaliação.
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('IMDB_Rating:Q')
)
Com os dados agrupados, vamos agora resumir a distribuição das avaliações do Rotten Tomatoes. Por enquanto, removeremos as avaliações do IMDB e, em vez disso, usaremos o canal de codificação y para mostrar uma contagem agregada de registros (count), de modo que a posição vertical de cada ponto indique o número de filmes em cada intervalo de avaliação do Rotten Tomatoes.
Como a agregação count conta o número total de registros em cada intervalo independentemente dos valores dos campos, não precisamos incluir um nome de campo na codificação y.
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('count()')
)
Para chegarmos a um histograma padrão, vamos mudar o tipo de marca de circle para bar:
alt.Chart(movies_url).mark_bar().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('count()')
)
Agora podemos examinar a distribuição das avaliações com mais clareza: vemos menos filmes na extremidade negativa e um pouco mais na extremidade alta, mas, no geral, a distribuição é relativamente uniforme. As avaliações do Rotten Tomatoes são determinadas com base nos julgamentos de críticos de cinema, classificando os filmes como “positivo” ou “negativo” e calculando a porcentagem de críticas positivas. Parece que essa abordagem faz um bom trabalho ao utilizar toda a faixa de valores de avaliação.
Da mesma forma, podemos criar um histograma para as avaliações do IMDB apenas alterando o campo no canal de codificação x:
alt.Chart(movies_url).mark_bar().encode(
alt.X('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('count()')
)
Em contraste com a distribuição mais uniforme que vimos antes, as avaliações do IMDB exibem uma distribuição em forma de sino (embora negativamente assimétrica). As avaliações do IMDB são calculadas a partir da média das notas (variando de 1 a 10) fornecidas pelos usuários do site. Podemos perceber que essa forma de medição resulta em uma distribuição diferente das avaliações do Rotten Tomatoes. Também podemos observar que a moda da distribuição está entre 6,5 e 7: as pessoas geralmente gostam de assistir a filmes, o que pode explicar esse viés positivo!
Agora, vamos voltar ao nosso gráfico de dispersão das avaliações do Rotten Tomatoes e do IMDB. Veja o que acontece se agruparmos ambos os eixos do nosso gráfico original.
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
)
Detalhes são perdidos devido ao excesso de sobreposição, com muitos pontos desenhados diretamente uns sobre os outros.
Para formar um histograma bidimensional, podemos adicionar uma contagem agregada (count), como fizemos antes. Como os canais de codificação x e y já estão ocupados, devemos usar um canal de codificação diferente para representar as contagens. Aqui está o resultado de usar a área dos círculos adicionando um canal de codificação de tamanho.
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Size('count()')
)
Alternativamente, podemos representar as contagens usando o canal de cor (color) e alterar o tipo de marca para barra (bar). O resultado é um histograma bidimensional no formato de um mapa de calor.
alt.Chart(movies_url).mark_bar().encode(
alt.X('Rotten_Tomatoes_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Y('IMDB_Rating:Q', bin=alt.BinParams(maxbins=20)),
alt.Color('count()')
)
Compare os histogramas 2D baseados em tamanho e cor acima. Qual codificação você acha que deve ser preferida? Por quê? Em qual gráfico você consegue comparar com mais precisão a magnitude dos valores individuais? Em qual gráfico você consegue visualizar com mais precisão a densidade geral das avaliações?
Unidades de tempo#
Agora, vamos fazer uma pergunta completamente diferente: as bilheteiras variam conforme a temporada?
Para obter uma resposta inicial, vamos traçar um gráfico da mediana da receita bruta nos EUA por mês.
Para criar esse gráfico, utilizaremos a transformação timeUnit para mapear as datas de lançamento para o (month) mês do ano. O resultado é semelhante ao agrupamento, mas usa intervalos de tempo significativos. Outras unidades de tempo válidas incluem: year, quarter, date (dia numérico do mês), day (dia da semana) e hours, além de unidades compostas, como yearmonth ou hoursminutes. Consulte a documentação do Altair para ver a lista completa de unidades de tempo disponíveis.
alt.Chart(movies_url).mark_area().encode(
alt.X('month(Release_Date):T'),
alt.Y('median(US_Gross):Q')
)
Observando o gráfico resultante, as vendas medianas de filmes nos EUA parecem aumentar durante a temporada de blockbusters no verão e no período de férias de fim de ano. É claro que pessoas ao redor do mundo (não apenas nos EUA) vão ao cinema. Um padrão semelhante ocorre na receita bruta mundial?
alt.Chart(movies_url).mark_area().encode(
alt.X('month(Release_Date):T'),
alt.Y('median(Worldwide_Gross):Q')
)
Sim!
Técnicas Avançadas de Transformação de Dados#
Todos os exemplos acima usam transformações (bin, timeUnit, aggregate, sort) que são definidas relativo a um canal de codifcação. Contudo, as vezes você quer aplicar uma cadeia de transformações antes da visualização, ou talvez usar transformações que não integram nas definições de codificação do Altair. Para esses casos específicos, o Altair e o Vega-Lite disponibiliza transformações de dados que estão definidas separadamente das codificações. Essas transformações então serão aplicadas aos dados antes de quaisquer codifcações forem aplicadas.
Poderiamos também fazer transformações usando o Pandas diretamente, e depois visualizar o resultado. Entretanto, usar as transformações já embutidas no Altair permite nossas visualizações serem publicadas mais facilmente em alguns contextos; por exemplo, exportar o arquivo Vega-Lite em JSON para usar em alguma aplicação no navegador web. Vamos dar uma olhada nessas transformações que o Altair permite, como o calculate, filter, aggregate, e window.
Calcular#
Você se lembra da nossa comparação da arrecadação nos Estados Unidos com a arrecadação mundial? Teoricamente, a arrecadação mundial não considera também a arrecadação dos Estados Unidos? (realmente considera) Como podemos ter uma ideia das tendências fora dos EUA?
Com a transformação calculate podemos criar novos conjuntos de dados. Aqui queremos subtrair a arrecadação dos Estados Unidos da arrecadação mundial. A transformação pega uma expressão em string do Vega para definir a fórmula durante um unico recorte. Expressões no Vega usam a sintaxe do Javascript. O prefixo datum. acessa o conjunto com o input dado.
alt.Chart(movies_url).mark_area().transform_calculate(
NonUS_Gross='datum.Worldwide_Gross - datum.US_Gross'
).encode(
alt.X('month(Release_Date):T'),
alt.Y('median(NonUS_Gross):Q')
)
Podemos ver que as tendências sazonais existem fora dos EUA, porém com um declive maior nos demais meses.
Filtrar#
A transformação filter cria uma nova tabela com um subconjunto dos dados originais, removendo linhas que falham em um teste de predicado fornecido. Similar à transformação calculate, predicados de filtro são expressos usando a linguagem de expressão Vega.
Abaixo adicionamos um filtro para limitar nosso gráfico de dispersão (scatter plot) inicial das avaliações do IMDB vs. Rotten Tomatoes para somente filmes com gênero principal “Romântic Comedy”.
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q'),
alt.Y('IMDB_Rating:Q')
).transform_filter('datum.Major_Genre == "Romantic Comedy"')
Como o gráfico muda se filtrarmos para ver outros gêneros? Edite a expressão de filtro para descobrir.
Agora vamos filtrar para ver filmes lançados antes de 1970.
List item
List item
alt.Chart(movies_url).mark_circle().encode(
alt.X('Rotten_Tomatoes_Rating:Q'),
alt.Y('IMDB_Rating:Q')
).transform_filter('year(datum.Release_Date) < 1970')
Eles parecem pontuar excepcionalmente bem! Filmes mais velhos são simplesmente melhores, ou há um viés de seleção apontando para mais filmes antigos bem avaliados nesse conjunto de dados?
Agregar#
Nós já vimos transformações aggregate tal como count e average no contexto de canais de codificação. Nós também podemos especificar agregados separadamente, como um passo de pré-processamento para outras transformações (como nos exemplos de transformação window abaixo). A saída de uma transformação aggregate é uma nova tabela de dados com registros que contém os campos groupby e as medidas aggregate computadas.
Vamos recriar nosso gráfico de avaliação média por gênero, mas dessa vez usando uma transformação aggregate separada. A tabela de saída da transformação aggregate contém 13 linhas, uma para cada gênero.
Para ordenar o eixo Y, devemos incluir uma operação aggregate separada nas nossas instruções de ordenação. Aqui nós usamos o operador max, que funciona bem porque há somente um registro de saída por gênero. Similarmente, nós poderíamos ter usado o operador min e ter acabado com o mesmo gráfico.
alt.Chart(movies_url).mark_bar().transform_aggregate(
groupby=['Major_Genre'],
Average_Rating='average(Rotten_Tomatoes_Rating)'
).encode(
alt.X('Average_Rating:Q'),
alt.Y('Major_Genre:N', sort=alt.EncodingSortField(
op='max', field='Average_Rating', order='descending'
)
)
)
Janela#
A transformação window realiza cálculos sobre grupos classificados de registros de dados. As transformações de janela são bastante poderosas, suportando tarefas como classificação, análise de lead/lag, totais cumulativos e somas ou médias correntes. Os valores calculados por uma transformação window são gravados de volta na tabela de dados de entrada como novos campos. As operações de janela incluem as operações agregadas que vimos anteriormente, bem como operações especializadas como rank, row_number, lead e lag. A documentação do Vega-Lite lista todas as operações válidas de janela.
Um caso de uso para uma transformação window é calcular listas top-k. Vamos plotar os 20 principais diretores em termos de total bruto mundial.
Primeiro usamos uma transformação filter para remover registros para os quais não conhecemos o diretor. Caso contrário, o diretor null dominaria a lista! Em seguida, aplicamos um aggregate para somar a receita bruta mundial de todos os filmes, agrupados por diretor. Neste ponto, poderíamos traçar um gráfico de barras classificado (sorted bar chart), mas acabaríamos com centenas e centenas de diretores. Como podemos limitar a exibição aos 20 primeiros?
A transformação window nos permite determinar os principais diretores calculando sua ordem de classificação. Dentro da nossa definição de transformação window, podemos sort por receita bruta e usar a operação rank para calcular as pontuações de classificação de acordo com essa ordem de classificação. Podemos então adicionar uma transformação filter subsequente para limitar os dados a apenas registros com um valor de classificação menor ou igual a 20.
alt.Chart(movies_url).mark_bar().transform_filter(
'datum.Director != null'
).transform_aggregate(
Gross='sum(Worldwide_Gross)',
groupby=['Director']
).transform_window(
Rank='rank()',
sort=[alt.SortField('Gross', order='descending')]
).transform_filter(
'datum.Rank < 20'
).encode(
alt.X('Gross:Q'),
alt.Y('Director:N', sort=alt.EncodingSortField(
op='max', field='Gross', order='descending'
))
)
Podemos ver que Steven Spielberg tem sido bem-sucedido em sua carreira! No entanto, mostrar somas pode favorecer diretores que tiveram carreiras mais longas e, portanto, fizeram mais filmes e, portanto, mais dinheiro. O que acontece se mudarmos a escolha da operação agregada? Quem é o diretor mais bem-sucedido em termos de média ou mediana bruta por filme? Modifique a transformação agregada acima!
Anteriormente neste capítulo, examinamos histogramas, que aproximam a função de densidade de probabilidade de um conjunto de valores. Uma abordagem complementar é observar a distribuição cumulativa. Por exemplo, pense em um histograma no qual cada bin inclui não apenas sua própria contagem, mas também as contagens de todos os bins anteriores — o resultado é um total corrente, com o último bin contendo o número total de registros. Um gráfico cumulativo nos mostra diretamente, para um dado valor de referência, quantos valores de dados são menores ou iguais a essa referência.
Como um exemplo concreto, vamos olhar para a distribuição cumulativa de filmes por tempo de execução (em minutos). Apenas um subconjunto de registros realmente inclui informações de tempo de execução, então primeiro filtramos para baixo para o subconjunto de filmes para os quais temos tempos de execução. Em seguida, aplicamos um aggregate para contar o número de filmes por duração (implicitamente usando “bins” de 1 minuto cada). Em seguida, usamos uma transformação window para calcular um total contínuo de contagens em bins, classificados por tempo de execução crescente.
alt.Chart(movies_url).mark_line(interpolate='step-before').transform_filter(
'datum.Running_Time_min != null'
).transform_aggregate(
groupby=['Running_Time_min'],
Count='count()',
).transform_window(
Cumulative_Sum='sum(Count)',
sort=[alt.SortField('Running_Time_min', order='ascending')]
).encode(
alt.X('Running_Time_min:Q', axis=alt.Axis(title='Duration (min)')),
alt.Y('Cumulative_Sum:Q', axis=alt.Axis(title='Cumulative Count of Films'))
)
Vamos examinar a distribuição cumulativa de durações de filmes. Podemos ver que filmes com menos de 110 minutos compõem cerca de metade de todos os filmes para os quais temos tempos de execução. Vemos um acúmulo constante de filmes entre 90 minutos e 2 horas, após o qual a distribuição começa a diminuir. Embora raro, o conjunto de dados contém vários filmes com mais de 3 horas de duração!
Resumo#
Nós apenas arranhamos a superfície do que as transformações de dados podem fazer! Para mais detalhes, incluindo todas as transformações disponíveis e seus parâmetros, veja a documentação de transformação de dados do Altair.
Às vezes, você precisará executar uma transformação de dados significativa para preparar seus dados antes de usar ferramentas de visualização. Para se envolver em disputa de dados aqui mesmo no Python, você pode usar a biblioteca Pandas.